package dryven.view.engine.parser;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import dryven.view.engine.component.ViewComponent;
import dryven.view.engine.component.metadata.AttributeDeclaration;
import dryven.view.engine.component.metadata.ViewComponentMetaDataStrategy;
import dryven.view.engine.expression.ViewDataDeclaration;
import dryven.view.engine.expression.nodelisters.ExpressionNodeFactory;
import dryven.view.engine.parser.componentinvocation.AttributeBinding;
import dryven.view.engine.parser.componentinvocation.AttributeBindings;
import dryven.view.engine.parser.componentinvocation.ChildZoneInitialization;
import dryven.view.engine.parser.componentinvocation.ComponentInvocationCompilationData;
import dryven.view.engine.parser.parts.ChildZonePart;
import dryven.view.engine.parser.parts.CompiledViewPart;
import dryven.view.engine.parser.parts.ComponentInvocationPart;
import dryven.view.engine.parser.parts.ExpressionPart;
import dryven.view.engine.parser.parts.TextPart;
import dryven.view.engine.parser.parts.ViewDataDeclarationPart;
import dryven.view.engine.parser.scope.InstructionScope;
import dryven.view.engine.parser.scope.PageScope;

public class ViewFileCompiler {
	
	private InputStream _input;
	private Iterable<String> _modelPackages;
	private ExpressionNodeFactory _expressionFactory;
	private InstructionScope _currentScope;
	private ViewComponentLocator _locator;
	private int _elementDepth = 0;
	private Stack<ComponentInvocationCompilationData> _componentInvocations = new Stack<ComponentInvocationCompilationData>();
	
	public ViewFileCompiler(InputStream input, ExpressionNodeFactory expressionFactory, Iterable<String> modelPackages) {
		super();
		_input = input;
		_modelPackages = modelPackages;
		_expressionFactory = expressionFactory;
	}

	public CompiledViewFile compile() throws CompilationException {
		try {
			PageScope pageScope = new PageScope();
			_currentScope = pageScope;
			_locator = new ViewComponentLocator();
			XmlProcessor processor = new XmlProcessor();
			SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
			parser.parse(_input, processor);
			CompiledViewFile file = new CompiledViewFile(pageScope.getParts(), _expressionFactory, _locator);
			/*for (CompiledViewPart part : pageScope.getParts()) {
				System.out.print(part.getClass().getName());
				if(part instanceof TextPart) {
					System.out.println(": "+((TextPart)part).getText());
				} else if(part instanceof ExpressionPart) {
					System.out.println(": "+((ExpressionPart)part).getExpression());
				} else {
					System.out.println("");
				}
			}*/
			file.compile();
			return file;
		} catch (ParserConfigurationException e) {
			throw new RuntimeException("XML parser not available", e);
		} catch (SAXException e) {
			throw new RuntimeException("Error while parsing view file", e);
		} catch(IOException e) {
			throw new RuntimeException("Error while reading document", e);
		} catch(RuntimeCompilationException e) {
			throw new CompilationException(e.getMessage(),e);
		}
	}
	
	protected void setScope(InstructionScope scope) {
		_currentScope = scope;
	}
	
	protected InstructionScope getScope() {
		return _currentScope;
	}
	

	protected void startComponentCall(String qName, Attributes attributes, ViewComponentMetaDataStrategy componentStrategy) {
		System.out.println(String.format("%s is a component call to strategy class %s", qName,componentStrategy.getClass().getName()));
		AttributeBindings attributeBindings = new AttributeBindings();
		for(int i=0;i<attributes.getLength();++i) {
			String name = attributes.getQName(i);
			String value = attributes.getValue(i);
			attributeBindings.addBinding(name, value);
		}
		/* for (AttributeDeclaration d : componentStrategy.getAttributeDeclarations()) {
			System.out.println("\t"+d.getName()+" as "+d.getValueType().getName());
		} */

		ComponentInvocationPart part = new ComponentInvocationPart(attributeBindings,componentStrategy);
		getScope().addPart(part);
		_componentInvocations.push(new ComponentInvocationCompilationData(componentStrategy, _elementDepth, part));
	}
	
	protected void startChildZone(String name, ComponentInvocationCompilationData data) {
		System.out.println("\tstarting child zone "+name);
		if(data.getCurrentChildZoneStatus()==ChildZoneInitialization.ImplicitlyInitialized) {
			endChildZone(ViewComponent.DefaultChildZone, data);
		}
		data.setCurrentChildZoneStatus(ChildZoneInitialization.Initialized);
		ChildZonePart czPart = data.getOrAddChildZonePart(name, getScope(), _expressionFactory);
		setScope(czPart);
	}

	protected void endChildZone(String name,
			ComponentInvocationCompilationData compilationData) {
		
		System.out.println("\tending child zone "+name);

		ChildZonePart part = (ChildZonePart) getScope();
		setScope(part.getParentScope());
		compilationData.setCurrentChildZoneStatus(ChildZoneInitialization.Unintialized);
	}

	protected void endComponentCall(String qName, ComponentInvocationCompilationData data) {
		if(data.getCurrentChildZoneStatus()==ChildZoneInitialization.ImplicitlyInitialized) {
			endChildZone(ViewComponent.DefaultChildZone, data);
		}
		_componentInvocations.pop();
		System.out.println("Component end: "+qName);

	}
	
	protected void contentEndElement(String uri, String localName,
			String qName) {
		getScope().addPart(new TextPart(String.format("</%s>", qName)));
	}
	
	protected void contentText(String text) {
		Iterable<CompiledViewPart> parts = parseExpressionText(text);
		for (CompiledViewPart part : parts) {
			getScope().addPart(part);
		}
	}

	protected void contentStartElement(String uri, String localName,
			String qName, Attributes attributes) {
		getScope().addPart(new TextPart("<"+qName));
		for(int i=0;i<attributes.getLength();++i) {
			String name = attributes.getQName(i);
			String value = attributes.getValue(i);
			getScope().addPart(new TextPart(String.format(" %s=\"", name)));
			Iterable<CompiledViewPart> valueParts = parseExpressionText(value);
			for (CompiledViewPart valuePart : valueParts) {
				getScope().addPart(valuePart);
			}
			getScope().addPart(new TextPart("\""));
			
		}
		getScope().addPart(new TextPart(">"));
	}

	protected void addViewDataDeclarations(Attributes attributes) {
		for(int i=0;i<attributes.getLength();++i) {
			String name = attributes.getQName(i);
			String type = normalizeTypeDeclaration(attributes.getValue(i));
			getScope().addPart(new ViewDataDeclarationPart(name, type));
		}
	}

	protected void handleRootElement(Attributes attributes) {
		for(int i=0;i<attributes.getLength();++i) {
			String attrQName = attributes.getQName(i);
			if(attrQName.startsWith("xmlns:")) {
				String namespace = attrQName.substring("xmlns:".length());
				String packageName = attributes.getValue(i);
				_locator.bindPackage(namespace, packageName);
			}
		}
	}
	
	protected Iterable<CompiledViewPart> parseExpressionText(String text) {
		List<CompiledViewPart> parts = new LinkedList<CompiledViewPart>();
		char prevChar = 0x00;
		boolean inExpression = false;
		int bracketsDepth = 0;
		int partStart = 0;
		for(int i=0;i<text.length();++i) {
			char c = text.charAt(i);
			if(c=='{' && prevChar=='$') {
				if(i!=partStart) {
					parts.add(new TextPart(text.substring(partStart,i-1)));
				}
				inExpression = true;
				bracketsDepth = 1;
				partStart = i+1;
			}
			else if(inExpression && c=='{') {
				++bracketsDepth;
			}
			else if(inExpression && c=='}') {
				--bracketsDepth;
				if(bracketsDepth==0) {
					//end of expression
					inExpression = false;
					if(i!=partStart) {
						parts.add(new ExpressionPart(text.substring(partStart,i)));
					}
					partStart = i+1;
				}
			}
			prevChar = c;
		}
		if(inExpression) {
			throw new RuntimeCompilationException(String.format("Expression not closed: %s", text));
		}
		if(partStart!=text.length()) {
			parts.add(new TextPart(text.substring(partStart,text.length())));
		}
		return parts;
	}
	//TODO return TypeRef
	protected String normalizeTypeDeclaration(String type) {
		type = type.replaceAll("\\[", "<");
		type = type.replaceAll("\\]", ">");

		String format = "%s";
		//iterable?
		if(type.endsWith("*")) {
			format = Iterable.class.getName()+"<%s>";
			type = type.substring(0, type.length()-1);
		}
		//try if type is a fully qualified classname ..
		Class<?> typeClass = null;
		try {
			typeClass = Class.forName(type);
		} catch (ClassNotFoundException e) {}
		//... or try one of the supplied packages
		if(typeClass==null) {
			for (String packageName : _modelPackages) {
				try {
					typeClass = Class.forName(packageName+"."+type);
					break;
				} catch (ClassNotFoundException e) {}
			}
		}
		return String.format(format,typeClass.getName());
	}


	class XmlProcessor extends DefaultHandler {
		
		private boolean _rootElementHandled = false;
		
		public XmlProcessor() {
		}
		
		@Override
		public void characters(char[] ch, int start, int length)
				throws SAXException {
			contentText(new String(ch,start,length));
		}

		@Override
		public void endDocument() throws SAXException {
			super.endDocument();
		}

		@Override
		public void endElement(String uri, String localName, String qName)
				throws SAXException {
			
			try {
				if(qName.equals("viewdata")) {
					return;
				}
				ComponentInvocationCompilationData compilationData = _componentInvocations.peek();
				
				if(compilationData!=null) {
					if(compilationData.isComponentEnd(_elementDepth)) {
						endComponentCall(qName, compilationData);
					}
					else if(compilationData.elementIsChildZone(_elementDepth, qName)) {
						endChildZone(qName, compilationData);
					} else {
						contentEndElement(uri, localName, qName);
					}
				} 
				else {
					contentEndElement(uri, localName, qName);
				}
			} finally {
				--_elementDepth;
			}
		}

		@Override
		public void ignorableWhitespace(char[] ch, int start, int length)
				throws SAXException {
			getScope().addPart(new TextPart(new String(ch,start,length)));
		}

		@Override
		public void startDocument() throws SAXException {
			super.startDocument();
		}

		@Override
		public void startElement(String uri, String localName, String qName,
				Attributes attributes) throws SAXException {
			++_elementDepth;
			
			//if this is the root element, look for package definitions
			if(!_rootElementHandled) {
				handleRootElement(attributes);
				_rootElementHandled = true;
			}
			
			ComponentInvocationCompilationData compilationData = null;
			if(!_componentInvocations.isEmpty()) {
				compilationData = _componentInvocations.peek();
			}
			
			//is this element a view data declaration?
			if(qName.equals("viewdata")) {
				startImplicitDefaultChildZoneIfNeccesary(compilationData);
				addViewDataDeclarations(attributes);
			} else {
				//look if the element is text or a component invocation
				String[] tagParts = qName.split(":");

				ViewComponentMetaDataStrategy componentStrategy = null;
				if(tagParts.length==2 && (componentStrategy=_locator.resolveComponent(tagParts[0], tagParts[1]))!=null) {
					startImplicitDefaultChildZoneIfNeccesary(compilationData);
					startComponentCall(qName, attributes, componentStrategy);
				} else {
					/*
					 * This seems to look like a content tag, but it could also be the begin
					 * of a child view zone. Let's check ...
					 */
					if(compilationData!=null && compilationData.elementIsChildZone(_elementDepth, qName)) {
						startChildZone(qName, compilationData);
					} else {
						startImplicitDefaultChildZoneIfNeccesary(compilationData);
						contentStartElement(uri, localName, qName, attributes);
					}
				}
			}
		}

		private void startImplicitDefaultChildZoneIfNeccesary(
				ComponentInvocationCompilationData compilationData) {
			//only the default childzone does not initialize itself when it is used implicitly (not with <default>)
			if(compilationData!=null && compilationData.getCurrentChildZoneStatus()==ChildZoneInitialization.Unintialized) {
				startChildZone(ViewComponent.DefaultChildZone, compilationData);
				compilationData.setCurrentChildZoneStatus(ChildZoneInitialization.ImplicitlyInitialized);
			}
		}

	}
	
	
}
